/*******************************************************************************
 * Copyright (c) 2012-2017 Codenvy, S.A.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *   Codenvy, S.A. - initial API and implementation
 *******************************************************************************/
package org.eclipse.che.plugin.machine.ssh.exec;

import com.google.common.util.concurrent.ThreadFactoryBuilder;

import org.eclipse.che.api.agent.server.terminal.WebsocketTerminalFilesPathProvider;
import org.eclipse.che.api.agent.shared.model.Agent;
import org.eclipse.che.api.agent.shared.model.impl.AgentImpl;
import org.eclipse.che.api.core.ConflictException;
import org.eclipse.che.api.core.ServerException;
import org.eclipse.che.api.core.model.machine.Command;
import org.eclipse.che.api.core.util.AbstractLineConsumer;
import org.eclipse.che.api.core.util.LineConsumer;
import org.eclipse.che.api.core.util.ListLineConsumer;
import org.eclipse.che.api.machine.server.exception.MachineException;
import org.eclipse.che.api.machine.server.model.impl.CommandImpl;
import org.eclipse.che.commons.lang.concurrent.LoggingUncaughtExceptionHandler;
import org.eclipse.che.commons.lang.concurrent.ThreadLocalPropagateContext;
import org.eclipse.che.plugin.machine.ssh.SshMachineInstance;
import org.eclipse.che.plugin.machine.ssh.SshMachineProcess;
import org.slf4j.Logger;

import javax.inject.Inject;
import javax.inject.Named;
import java.io.IOException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static com.google.common.base.Strings.isNullOrEmpty;
import static java.lang.String.format;
import static org.slf4j.LoggerFactory.getLogger;

/**
 * Launch exec agent in ssh machines.
 *
 * @author Alexander Garagatyi
 * @author Anatolii Bazko
 */
public class SshMachineExecAgentLauncher  {
    private static final Logger LOG = getLogger(SshMachineExecAgentLauncher.class);
    // Regex to parse output of command 'uname -sm'
    // Consists of:
    // 1. named group 'os' that contains 1+ non-space characters
    // 2. space character
    // 3. named group 'architecture' that contains 1+ non-space characters
    private static final Pattern         UNAME_OUTPUT         = Pattern.compile("\\[STDOUT\\] (?<os>[\\S]+) (?<architecture>[\\S]+)");
    private static final String          DEFAULT_ARCHITECTURE = "linux_amd64";
    private static final ExecutorService executor             =
            Executors.newCachedThreadPool(new ThreadFactoryBuilder().setNameFormat("SshAgentLauncher-%d")
                                                                    .setUncaughtExceptionHandler(
                                                                            LoggingUncaughtExceptionHandler.getInstance())
                                                                    .setDaemon(true)
                                                                    .build());

    private final WebsocketTerminalFilesPathProvider archivePathProvider;
    private final String                             terminalLocation;
    private final long                               agentMaxStartTimeMs;
    private final long                               agentPingDelayMs;
    private final String                             terminalRunCommand;

    @Inject
    public SshMachineExecAgentLauncher(@Named("che.agent.dev.max_start_time_ms") long agentMaxStartTimeMs,
                                       @Named("che.agent.dev.ping_delay_ms") long agentPingDelayMs,
                                       @Named("machine.ssh.server.terminal.location") String terminalLocation,
                                       @Named("machine.terminal_agent.run_command") String terminalRunCommand,
                                       WebsocketTerminalFilesPathProvider terminalPathProvider) {
        this.agentMaxStartTimeMs = agentMaxStartTimeMs;
        this.agentPingDelayMs = agentPingDelayMs;
        this.terminalRunCommand = terminalRunCommand;
        this.archivePathProvider = terminalPathProvider;
        this.terminalLocation = terminalLocation;
    }

    public void launch(SshMachineInstance machine, Agent agent) throws ServerException {
        if (isNullOrEmpty(agent.getScript())) {
            return;
        }
        try {
            String architecture = detectArchitecture(machine);
            machine.copy(archivePathProvider.getPath(architecture), terminalLocation);
            final AgentImpl agentCopy = new AgentImpl(agent);
            agentCopy.setScript(agent.getScript() + "\n" + terminalRunCommand);

            final SshMachineProcess process = start(machine, agentCopy);
            LOG.debug("Waiting for agent {} is launched. Workspace ID:{}", agentCopy.getId(), machine.getWorkspaceId());

            final long pingStartTimestamp = System.currentTimeMillis();
            SshProcessLaunchedChecker agentLaunchingChecker = new SshProcessLaunchedChecker("che-websocket-terminal");
            while (System.currentTimeMillis() - pingStartTimestamp < agentMaxStartTimeMs) {
                if (agentLaunchingChecker.isLaunched(agentCopy, machine)) {
                    return;
                } else {
                    Thread.sleep(agentPingDelayMs);
                }
            }

            process.kill();

            final String errMsg = format("Fail launching agent %s. Workspace ID:%s", agent.getName(), machine.getWorkspaceId());
            LOG.error(errMsg);
            throw new ServerException(errMsg);
        } catch (MachineException e) {
            throw new ServerException(e.getServiceError());
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new ServerException(format("Launching agent %s is interrupted", agent.getName()));
        } catch (ConflictException e) {
            // should never happen
            throw new ServerException("Internal server error occurs on terminal launching.");
        }
    }

    private String detectArchitecture(SshMachineInstance machine) throws ConflictException, MachineException {
        // uname -sm shows OS and CPU architecture
        // Examples of output:
        // Windows 10 amd64
        // MSYS_NT-6.3 x86_64
        // (empty line)
        // Ubuntu Linux 14.04 amd64
        // Linux x86_64
        // OS X amd64
        // Darwin x86_64
        // Samsung Artik arm7
        // Linux armv7l
        SshMachineProcess getUnameOutput = machine.createProcess(new CommandImpl("discover machine architecture",
                                                                                 "uname -sm",
                                                                                 null),
                                                                 null);
        ListLineConsumer lineConsumer = new ListLineConsumer();
        getUnameOutput.start(lineConsumer);
        String unameOutput = lineConsumer.getText();
        Matcher matcher = UNAME_OUTPUT.matcher(unameOutput);
        if (matcher.matches()) {
            String os = matcher.group("os").toLowerCase();
            String arch = matcher.group("architecture").toLowerCase();
            StringBuilder result = new StringBuilder();
            if (os.contains("linux")) {
                result.append("linux_");
            } else if (os.contains("darwin")) {
                result.append("darwin_");
            } else if (os.contains("msys")) {
                result.append("windows_");
            } else {
                LOG.error(format("Architecture discovering fails. Machine %s. uname output:%s", machine.getId(), unameOutput));
                return DEFAULT_ARCHITECTURE;
            }
            if (arch.contains("x86_64")) {
                result.append("amd64");
            } else if (arch.contains("armv7l")) {
                result.append("arm7");
            } else if (arch.contains("armv6l")) {
                result.append("arm6");
            } else if (arch.contains("armv5l")) {
                result.append("arm5");
            } else {
                LOG.error(format("Architecture discovering fails. Machine %s. uname output:%s", machine.getId(), unameOutput));
                return DEFAULT_ARCHITECTURE;
            }

            return result.toString();
        } else {
            LOG.error(format("Architecture discovering fails. Machine %s. uname output:%s", machine.getId(), unameOutput));
            return DEFAULT_ARCHITECTURE;
        }
    }

    protected SshMachineProcess start(SshMachineInstance machine, Agent agent) throws ServerException {
        Command command = new CommandImpl(agent.getId(), agent.getScript(), "agent");
        SshMachineProcess process = machine.createProcess(command, null);
        LineConsumer lineConsumer = new AbstractLineConsumer() {
            @Override
            public void writeLine(String line) throws IOException {
                machine.getLogger().writeLine(line);
            }
        };

        CountDownLatch countDownLatch = new CountDownLatch(1);
        executor.execute(ThreadLocalPropagateContext.wrap(() -> {
            try {
                countDownLatch.countDown();
                process.start(lineConsumer);
            } catch (ConflictException | MachineException e) {
                try {
                    machine.getLogger().writeLine(format("[ERROR] %s", e.getMessage()));
                } catch (IOException ignored) {
                }
            } finally {
                try {
                    lineConsumer.close();
                } catch (IOException ignored) {
                }
            }
        }));
        try {
            // ensure that code inside of task submitted to executor is called before end of this method
            countDownLatch.await();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }

        return process;
    }
}
